
#region Imported Namespaces
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;
#endregion

namespace PHSRAG.WebControls
{
	#region Class ExpandingDataGrid
	/// <summary>
	/// The ExpandingDataGrid is an extension of the DataGrid class which supports the expansion of each row by clicking on a
	/// column which displays a expand/collapse text/button (configurable).
	/// </summary>
	/// <remarks>
	/// <p>The ExpandingDataGrid only allows one level of nesting.</p>
	/// <p>The DataSource property for the ExpadingDataGrid only accepts the following types:</p>
	/// <list type="bullet">
	/// <item><description>DataSet (first DataTable will be used)</description></item>
	/// <item><description>DataTable</description></item>
	/// <item><description>ArrayList</description></item>
	/// </list>
	/// <p>Setting the DataSource property to any other will result in an exception being thrown.</p>
	/// </remarks>
	/// <example>
	/// The following example shows how a page would use this control. The registration of the control can be done
	/// simply by dropping the control on the page in design mode.
	/// <code>
	/// <![CDATA[
	/// <%@ Register TagPrefix="cc1" Namespace="PHSRAG.WebControls" Assembly="PHSRAG.WebControls" %>
	/// ...
	/// <body>
	/// ...
	/// <table>
	///		<tr>
	///			<td>
	///				<cc1:ExpandingDataGrid CssClass="DataGrid" id="expandingDataGrid" runat="server" AutoGenerateColumns="False"
	///						KeyColumnHeaderText="ID"
	///						CollapseText="&lt;img border=no src=images/collapseButton.gif&gt;"
	///						ExpandText="&lt;img border=no src=images/expandButton.gif&gt;">
	///					<AlternatingItemStyle CssClass="DataGridAlternatingItem"></AlternatingItemStyle>
	///					<ItemStyle CssClass="DataGridItem"></ItemStyle>
	///					<HeaderStyle CssClass="DataGridHeader"></HeaderStyle>
	///					<Columns>
	///						<asp:BoundColumn DataField="ID" HeaderText="ID"></asp:BoundColumn>
	///						<asp:BoundColumn DataField="Name" HeaderText="Name"></asp:BoundColumn>
	///					</Columns>
	///				</cc1:ExpandingDataGrid>
	///			</td>
	///		</tr>
	/// </table>
	/// ...
	/// </body>
	/// ]]>
	/// </code>
	/// </example>
	[ToolboxData("<{0}:ExpandingDataGrid runat=server />")]
	public class ExpandingDataGrid : DataGrid
	{
		#region Constants
		private const string ChildDataTable = "ChildDataTable_6D286C63-E7A7-40c8-8A55-A1EAB9AFE27D";
		private const string CollapseTextKey = "CollapseText_6D286C63-E7A7-40c8-8A55-A1EAB9AFE27D";
		private const string CommandName = "ExpandRow";
		private const string DataSourceKey = "DataSource_6D286C63-E7A7-40c8-8A55-A1EAB9AFE27D";
		private const string ExpandedTag = "--Expanded--";
		private const string ExpandTextKey = "ExpandText_6D286C63-E7A7-40c8-8A55-A1EAB9AFE27D";
		private const string KeyColumnHeaderTextKey = "KeyColumnHeaderText_6D286C63-E7A7-40c8-8A55-A1EAB9AFE27D";
		private const string StateColumn = "StateColumn";
        private const string ALL_NUMERIC_PATTERN = "[a-z|A-Z]";
		#endregion

		#region Instance Variables
		private int keyColumnIndexInDataGrid = -1;
		private int keyColumnIndexInDataTable = -1;
        private static readonly Regex All_Numeric_Regex = new Regex(ALL_NUMERIC_PATTERN);
		#endregion

		#region Delegates & Events
		/// <summary>
		/// The ItemExpandedHandler delegate is used to propagate the expansion/collapse of a row. The return type must
		/// be one of DataSet, DataTable, or ArrayList.
		/// </summary>
		public delegate object ItemExpandedHandler(string key);
 
        /// <summary>
		/// The ItemExpanded event is raised whenever a row is either expanded or collapsed.
		/// </summary>
		[Category("Action"), Description("Event raised when an item in the ExpandingDataGrid is expanded/collapsed")]
		public event ItemExpandedHandler ItemExpanded;
		#endregion

		#region Public Properties
		/// <summary>
		/// Get/Set the text displayed when a row is collapsed. State is maintained within the ViewState.
		/// </summary>
		[Category("Appearance"), Description("Get/Set the text displayed when a row is collapsed")]
		public string CollapseText 
		{
			get { string text = ViewState[CollapseTextKey] as string; return (text == null) ? "-" : text; }
			set { if (!IsDesignMode) ViewState[CollapseTextKey] = value; }
		}

		/// <summary>
		/// Get/Set the text displayed when a row is expanded. State is maintained within the ViewState.
		/// </summary>
		[Category("Appearance"), Description("Get/Set the text displayed when a row is expanded")]
		public string ExpandText
		{
			get { string text = ViewState[ExpandTextKey] as string; return (text == null) ? "+" : text; }
			set { if (!IsDesignMode) ViewState[ExpandTextKey] = value; }
		}

		/// <summary>
		/// Get/Set the header text of the column which contains the unique key for a row. The column may be invisible.
		/// </summary>
		[Category("Behavior"), Description("Get/Set the header text of the column which contains the unique key for a row")]
		public string KeyColumnHeaderText
		{
			get { return ViewState[KeyColumnHeaderTextKey] as string; }
			set	{ if (!IsDesignMode) ViewState[KeyColumnHeaderTextKey] = value; }
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Reset the state of the ExpandingDataGrid.
		/// </summary>
		public void Reset()
		{
			CurrentPageIndex = 0;
			Page.Session.Remove(DataSourceKey);
		}
		#endregion

		#region Overrides
		/// <summary>
		/// Get/Set the data source for the data grid.
		/// </summary>
		/// <remarks>
		/// <p>The ExpandingDataGrid only accepts a DataTable as its data source. An exception is thrown if any other type is
		/// specified.</p>
		/// <p>The data source is maintained in the cache specified through the PageCachePolicy. This is done to allow the
		/// ExpandingDataGrid to maintain additional state along with the specified data source. The additional state takes
		/// the form of a column referred to as the <b>StateColumn</b>.</p>
		/// <p>The usage of the contents of the StateColumn depends on whether the row is a parent row or a child row.
		/// In this context, a parent row is the one which is expandable. Child rows are not expandable.</p>
		/// <p>In the context of a parent row, the StateColumn has one of two values: DBNull when the row is collapsed,
		/// or the ExpandedTag when the row is expanded.</p>
		/// <p>In the context of a child row, the StateColumn holds the value of the key column from its parent row. This
		/// allows all children to be grouped with a single parent.</p>
		/// </remarks>
		[Category("Behavior"), Description("Get/Set the data source for the data grid")]
		public override object DataSource
		{
			get
			{
				return IsDesignMode ? null : Page.Session[DataSourceKey];
			}
			set
			{
				if (!IsDesignMode)
				{
					DataTable dataTable = ConvertToDataTable(value);
					if (!dataTable.Columns.Contains(StateColumn))
						dataTable.Columns.Add(StateColumn);

					Page.Session[DataSourceKey] = base.DataSource = dataTable;
				}
			}
		}

        /// <summary>
        /// Resort the data and bind it.
        /// </summary>
        public override void DataBind()
        {
            // we refresh the expanded table on every DataBind() to prevent
            // strange formatting issues when the control is reloaded
            // with child tables in the wrong order (see TrackIt issue [3991])
            DataTable dataTable = (DataTable)DataSource;
            DataTable newSourceTable = dataTable.Clone();
            foreach (DataRow row in dataTable.Rows)
                newSourceTable.ImportRow(row);

            DataSource = newSourceTable;

            base.DataBind();
        }


		/// <summary>
		/// Initialize data grid.
		/// </summary>
		/// <param name="e">Argument associated with the event</param>
		/// <remarks>
		/// <p>The ExpandingDataGrid can only behave like one when a value is specified for the KeyColumnHeaderText property.
		/// If that is indeed the case, a new column is added as the first column to the columns collection. This column
		/// hosts a link button to allow the expand/collapse actions.</p>
		/// <p>Note that the index of the specified key column is not the same within the data table (underlying the
		/// data grid) and within a data grid row. This is because we add the expand/collapse column to the data grid.
		/// So as to speed up accessing the key column for both the data table and the data grid, their index is computed
		/// during initialization. The index of the key column is simply one less than the index of that column in the
		/// data grid.</p>
		/// </remarks>
		protected override void OnInit(EventArgs e)
		{
			base.OnInit(e);

			string keyColumnHeaderText = KeyColumnHeaderText;
			if (keyColumnHeaderText == null)
				throw new Exception("ExpandingDataGrid requires the KeyColumnHeaderText to be specified");

			ButtonColumn expandColumn = new ButtonColumn();
			expandColumn.ButtonType = ButtonColumnType.LinkButton;
			expandColumn.CommandName = CommandName;
			expandColumn.Text = ExpandText;
			Columns.AddAt(0, expandColumn);

			foreach (DataGridColumn column in Columns)
			{
				if (column.HeaderText == keyColumnHeaderText)
				{
					keyColumnIndexInDataGrid = Columns.IndexOf(column);
					keyColumnIndexInDataTable = keyColumnIndexInDataGrid - 1;
					break;
				}
			}
		}

		/// <summary>
		/// Handler for the event raised by the data grid when any button within the grid is clicked. We are interested
		/// in only handling the click on the expand/collpase button.
		/// </summary>
		/// <param name="e">Container of event data</param>
		/// <remarks>
		/// <p>If an event handler has been registered to handle the expand/collapse event, and if the event source is
		/// the expand/collapse column, the following actions are taken:</p>
		/// <list type="bullet">
		/// <item><description>Get the key associated with the data grid item which raised the event.</description></item>
		/// <item><description>Get the DataRow from the data source associated with that key (this is referred to as the
		/// "parent row".</description></item>
		/// <item><description><para>Find out if the row is being expanded or collapsed. This is done as follows:</para>
		/// <para>If the StateColumn of the row has DBNull as its value, then the row is being expanded.</para>
		/// <para>If the StateColumn of the row holds any other value, it is being collapsed.</para></description></item>
		/// <item><description>If the row is being expanded, the following actions are carried out:
		/// <para>Generate a key for the data table which holds the data to be shown below the row being expanded. This
		/// data table is referred to as the <b>Child Data Table</b>. The key is composed of a string tag and the key of
		/// the row being expanded.</para>
		/// <para>An attempt is made to retrieve the child data table from the cache. If it is not present there, then
		/// the delegate registered to handle this event is invoked so that the entity which hosts the ExpandingDataGrid
		/// may return a data table.</para>
		/// <para>If a valid data table has been retrieved, each row from the child data table is imported into the
		/// parent data table. These rows are added in sequence starting just below the row being expanded. The StateColumn
		/// of each such imported row is set to the key of the row being expanded.</para>
		/// <para>The StateColumn of the row being expanded is set to the expanded tag.</para>
		/// </description></item>
		/// <item><description>If the row is being collapsed, the following actions are carried out:
		/// <para>Remove all rows whose StateColumn value matches the key of the row being collapsed.</para>
		/// <para>The StateColumn of the row being collapsed is set to DBNull.</para>
		/// </description></item>
		/// <item><description>Finally, the data table is cloned (to preserve the row sequence), cached, and the data grid
		/// is rebound.</description></item>
		/// </list>
		/// </remarks>
		protected override void OnItemCommand(DataGridCommandEventArgs e)
		{
			base.OnItemCommand(e);

			if ((e.Item.ItemType == ListItemType.Item) || (e.Item.ItemType == ListItemType.AlternatingItem))
			{
				if ((ItemExpanded != null) && (e.CommandName == CommandName))
				{
					DataTable dataTable = (DataTable)DataSource;
					if (dataTable != null)
					{
						string keyColumnHeaderText = KeyColumnHeaderText;
						string key = dataTable.Rows[e.Item.ItemIndex][keyColumnHeaderText].ToString();

						DataRow[] rows = dataTable.Select(string.Format("{0}=" + (All_Numeric_Regex.IsMatch(key)?"'{1}'":"{1}"), keyColumnHeaderText, key));
						DataRow parentRow = rows[0];

						if (parentRow[StateColumn] == DBNull.Value)
						{
							DataTable childDataTable = ConvertToDataTable(ItemExpanded(key));
							if (childDataTable != null)
							{
								DataView view = new DataView(dataTable);
								view.Sort = keyColumnHeaderText;
								int index = view.Find(parentRow.ItemArray[keyColumnIndexInDataTable]);

								foreach (DataRow row in childDataTable.Rows)
								{
									DataRow childRow = dataTable.NewRow();
									childRow[StateColumn] = key;

									foreach (DataColumn c in childDataTable.Columns)
										childRow[c.ColumnName] = row[c.ColumnName];

									dataTable.Rows.InsertAt(childRow, ++index);
								}

								parentRow[StateColumn] = ExpandedTag;
							}
						}
						else
						{
							DataRow[] childRows = dataTable.Select(string.Format("{0}='{1}'", StateColumn, key));
							foreach (DataRow r in childRows)
								dataTable.Rows.Remove(r);
							parentRow[StateColumn] = DBNull.Value;
						}

						DataTable expandedTable = dataTable.Clone();
						foreach (DataRow row in dataTable.Rows)
							expandedTable.ImportRow(row);
						
						DataSource = expandedTable;
					}
					DataBind();
				}
			}
		}

		/// <summary>
		/// Handler for the event raised when a data grid item has been bound with data.
		/// </summary>
		/// <param name="e">Argument associated with the event</param>
		/// <remarks>
		/// <p>During the data binding, the following actions are taken:</p>
		/// <list type="bullet">
		/// <item><description>Retrieve the value of the state column from the data item.</description></item>
		/// <item><description>If the row is a parent row and is in an expanded state, set the expand/collapse column text
		/// to indicate that it may be collapsed (this column always starts of displaying that it may be expanded - during
		/// initialization of the data grid).</description></item>
		/// <item><description>If the row is a child row, remove the contents of the expand/collapse column.</description></item>
		/// </list>
		/// </remarks>
		protected override void OnItemDataBound(DataGridItemEventArgs e)
		{
			base.OnItemDataBound(e);
			if ((e.Item.ItemType == ListItemType.Item) || (e.Item.ItemType == ListItemType.AlternatingItem))
			{
				string stateTag = ((DataRowView)e.Item.DataItem)[StateColumn].ToString();
				if (!String.IsNullOrEmpty(stateTag))
				{
					if (stateTag == ExpandedTag)	// Parent row
						((LinkButton)e.Item.Cells[0].Controls[0]).Text = CollapseText;
					else	// Child row
						e.Item.Cells[0].Controls.Clear();
				}
			}
		}

		/// <summary>
		/// Handler for the event raised when pagination occurs.
		/// </summary>
		/// <param name="e">Argument associated with the event</param>
		/// <remarks>
		/// Set the current page index to indicate the page number to go to, and rebind the data grid.
		/// </remarks>
		protected override void OnPageIndexChanged(DataGridPageChangedEventArgs e)
		{
			base.OnPageIndexChanged(e);
			CurrentPageIndex = e.NewPageIndex;

			// The expression below seems ridiculous! Why bother setting something to itself? Well, before you dismiss it,
			// consider the fact that what is being used on both sides of the expression is a property. It simply retrieves
			// the cached version of the data table and binds that to the data grid.

			DataSource = DataSource;
			DataBind();
		}
		#endregion

		#region Private Properties
		/// <summary>
		/// Determine if the control is in design mode. It is if there is not HTTP context.
		/// </summary>
		private static bool IsDesignMode
		{
			get { return HttpContext.Current == null; }
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Convert one of the supported types of object to a DataTable. As of this writing, only the following types
		/// are acceptable: DataSet, DataTable, ArrayList.
		/// </summary>
		/// <param name="dataSource">Reference to the object of a supported type for conversion</param>
		/// <returns>DataTable containing the same data as the original object</returns>
		/// <remarks>
		/// <p>When a DataSet is specified for conversion, the first DataTable from its Tables collection is returned.</p>
		/// <p>When a DataTable is specified for conversion, it itself will be returned.</p>
		/// <p>When an ArrayList is specified, Refelection is used to figure out the names of all public properties of
		/// objects within the ArrayList. Each such property name becomes a column in the DataTable. The DataTable is
		/// then loaded with the value of each property for each object in the ArrayList.</p>
		/// </remarks>
		private static DataTable ConvertToDataTable(object dataSource)
		{
            DataSet ds = dataSource as DataSet;
			if (ds != null)
				return ds.Tables[0];

            DataTable dt = dataSource as DataTable;
            if (dt != null)
				return dt;

            ArrayList list = dataSource as ArrayList;
            if (list != null)
			{
				DataTable dataTable = new DataTable();

				IEnumerator enumerator = list.GetEnumerator();
				while (enumerator.MoveNext())
				{
					object o = enumerator.Current;
					PropertyInfo[] properties = o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
					
					if (dataTable.Columns.Count == 0)
						foreach (PropertyInfo p in properties)
							dataTable.Columns.Add(p.Name);

					DataRow row = dataTable.NewRow();
					foreach (PropertyInfo p in properties)
						row[p.Name] = p.GetValue(o, null);
					dataTable.Rows.Add(row);
				}
				return dataTable;
			}

			throw new Exception(string.Format("ExpandedDataGrid: {0} is not a supported type of DataSource", dataSource.GetType()));
		}
		#endregion
	}
	#endregion
}

// --- EOF ---
